本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。
購書連結 https://www.tenlong.com.tw/products/9789864344130
讓我們再次重新認識 JavaScript!
在介紹完變數與資料型別之後,本來想再繼續往運算式寫去,但我昨晚 人中之龍極二打到一半 突然想起有個很重要的部分遺漏了,那就在今天用一篇文章的篇幅來說明吧。
在前面幾天的文章當中,我們一直強調在 JavaScript 的資料可以分成「基本型別」(Primitives) 與「物件型別」(Object) 兩大類。
基本型別內的資料,會是以純值的形式存在 ( string
、number
、boolean
、null
、undefined
),而物件型別指的是可能由零或多種不同型別 (包括純值與物件) 所組合成的物件。
當我們今天要給變數資料的時候,假設我們給兩個變數分別設定為 10
:
var a = 10;
var b = 10;
// 在 JavaScript 判斷是否「相等」用 " === "
// 後續提到運算子會詳細介紹。
console.log( a === b ); // true
在基本型別的時候,會認為這兩個變數的「值」是相等的。 這應該不難理解,因為兩個變數的數值都是 10
。
同樣地,在字串的情況下也是:
var a = 'Kuro';
var b = 'Kuro';
var c = 'Jack';
console.log( a === b ); // true
console.log( a === c ); // false
所以在基本型別,當我們判斷這兩個變數是否相等,看的是裡面的內容,也就是「值」。
在物件型別的狀況下就不同了。
這裡我們分別宣告兩個物件,也都有個 value
的屬性。
var obj1 = { value: 10 };
var obj2 = { value: 10 };
猜猜看, obj1 === obj2
的結果會是?
.
.
.
答案會是 false
。 想當然如果是 true 我就不用另外寫這篇了
剛接觸 JavaScript 的朋友可能無法理解這點,沒關係,我們繼續往下看。
在 JavaScript 的物件,我們可以把它看作是一個「實體 (instance)」,什麼意思呢,這裡我舉個例子。
假設我口袋裡有十塊錢,你口袋裡也有十塊錢。這樣我們就有二十塊錢(不是
那麼在正常情況下,我們各自的十塊錢可以買到的東西應該是一樣多的對吧?
這個時候,我可以說我們各自的十塊錢是「等值」的。 用程式碼來說,就像這樣:
var a = 10;
var b = 10;
console.log( a === b ); // true
那麼在「物件」的情況下呢?
剛剛說 JavaScript 的物件都應該看作是一個「實體」。
以「實體」的前提下,假設我在我口袋裡的十塊錢用麥克筆上面打個 X
,除非我是劉謙,此時你口袋的十塊錢應該是不可能有 X
的記號對吧?
// 兩個 coin 的價值都是 10,但 coin1 與 coin2 卻不是同一個實體。
var coin1 = { value: 10 };
var coin2 = { value: 10 };
console.log( coin1 === coin2 ); // false
// 我在 coin1 畫了一個 X
coin1.cross = true;
// coin2.cross 當然不可能會有東西
console.log( coin2.cross ); // undefined
當然 JavaScript 的物件沒這麼單純,這裡暫時用極簡化的例子幫助各位理解。
既然大家都知道,「變數」裡面的內容是可以被變動,那麼在理解了「基本型別」與「物件型別」在比較時的不同後,接著就來聊聊變數的更新與傳遞,這部分我們一樣分成「基本型別」與「物件型別」兩種來看。
還記得十塊錢的範例嗎,如同稍早所說,在基本型別的變數中,我們看的是變數裡頭的「值」。 換言之,我們在複製變數的時候,複製的也是那個變數的「值」:
var a = 10;
var b = a;
console.log( a ); // 10
console.log( b ); // 10
可以看到,變數 b
的值是透過複製變數 a
的值而來。
但並不代表當變數 a
更新之後,會去影響變數 b
的數值:
a = 100;
// 變數 b 依然是 10,而變數 a 變成了 100
console.log( a ); // 100
console.log( b ); // 10
簡單來說, var b = a;
表面上看起來變數 b
的內容是透過複製變數 a
而來,但此時若變數 a
的內容為基本型別時,實際上變數 b
是去建立了一個新的值,然後將變數 a
的內容複製了一份過來。
這時候 a
與 b
各自是獨立的。
所以當變數 a
的內容後來經過更新變成 100
之後,變數 b
的內容依舊保持原來的 10
而不受影響。
像這種情況,我們通常會稱作「傳值」 (pass by value)。
那麼換成了物件型別呢?
讓我們回到剛剛 coin 的例子,並且稍微修改一下:
var coin1 = { value: 10 };
var coin2 = coin1;
console.log( coin1.value ); // 10
console.log( coin2.value ); // 10
乍看之下與前面基本型別 (純值) 的情況沒什麼不同,但是:
coin1.value = 100;
console.log( coin1.value ); // 100
console.log( coin2.value ); // 100
當 coin1.value
的內容被更新了之後,連帶著 coin2.value
卻也跟著更新了。
而且此時,你透過 ===
去檢查兩者實體時,會發現 coin1
與 coin2
實際上是同一個實體!
console.log( coin1 === coin2 ); // true
聰明的你應該已經猜到,其實「物件」這類資料型態,在 JavaScript 中是透過「引用」的方式傳遞資料的。
什麼意思? 這裡我用兩張圖來表示:
var coin1 = { value: 10 };
首先我們建立起一個新的物件的時候,JavaScript 會在記憶體的某處建立起一個物件 (圖右側),然後再將這個 coin1
變數指向新生成的物件。
var coin2 = coin1;
接著,當我們宣告了第二個變數 coin2
之後,並且透過 =
將 coin2
指向 coin1
的位置。
接著當我們更新了 coin1.value
的內容後, coin2.value
的內容也理所當然地被更新了。
coin1.value = 100;
console.log( coin1.value ); // 100
console.log( coin2.value ); // 100
所以實際上可以看出,coin1
與 coin2
這兩個變數是指向同一個實體的。
像這種透過引用的方式來傳遞資料,接收的其實是引用的「參考」而不是值的副本時,
我們通常會稱作「傳址」 (pass by reference)。
所以我說那個 JavaScript 是「傳值」或「傳址」呢?
在大多數的情況下,基本型別是「傳值」,而物件型別會是「傳址」的方式,但凡事都有例外。
我們來看看下面這個例子:
var coin1 = { value: 10 };
function changeValue(obj) {
obj = { value: 123 };
}
changeValue(coin1);
console.log(coin1); // ?
猜猜看,經過 changeValue(coin1)
操作後的 coin1
會是什麼?
答案仍是 { value: 10 }
。
剛剛說過,物件型別會是「傳址」的方式來更新資料,那應該會是 { value: 123 }
才對,為什麼依然不變?
事實上,JavaScript 不屬於單純的傳值或傳址。
更準確一點來說,JavaScript 應該屬於透過 pass by sharing (還沒找到合適的中文翻譯) 來傳遞資料。
「傳值」或「傳址」對大多數的開發者來說應該都不陌生,那麼「pass by sharing」又是什麼呢?
「Pass by sharing」的特點在於,當 function
的參數,如 function changeValue(obj){ ... }
中的 obj
被重新賦值的時候,外部變數的內容是不會被影響的。
var coin1 = { value: 10 };
function changeValue(obj) {
obj = { value: 123 };
}
changeValue(coin1);
console.log(coin1); // 此時 coin1 仍是 { value: 10 }
如果不是重新賦值的情況,則又會回到大家所熟悉的狀況:
var coin1 = { value: 10 };
function changeValue(obj) {
// 僅更新 obj.value,並未重新賦值
obj.value = 123;
}
changeValue(coin1);
console.log(coin1); // 此時 coin1 則會變成 { value: 123 }
所以 JavaScript 到底屬於何種策略?
我認為 JavaScript 應該更屬於 Pass by sharing 的形式。
參考 ECMA-262-3 in detail. Chapter 8. Evaluation strategy 所說:
Regardless of usage concept of reference in this case, this strategy should not be confused with the “call by reference” discussed above. The value of the argument is not a direct alias, but the copy of the address.
由於在 JavaScript 的物件類型是可變的 (mutable),當物件更新時,會影響到所有引用這個物件的變數與其副本,修改時會變動到原本的參考,但當賦與新值時,會產生新的實體參考。
而基本型別則是不可變的 (immutable),當你更新了某個基本型別的值時,與那個值的副本完全無關:
var a = 10;
var b = a;
a = 100;
console.log(a); // 100
console.log(b); // 10
這個時候在基本型別的操作下,以 Pass by sharing 的行為來說,與 Pass by value 的結果是完全一樣的,修改時永遠只能賦與新值。
那麼以上就是今天分享的主題,感謝各位看到這裡,我要回去打電動了,明天見,掰。
感謝大大分享,原來有 call by sharing,
小弟有在網路上看到過相關文章,
內容講述,javascript 其實只有傳值呼叫,
物件的傳遞也只是參考的值的傳遞,
因為 javascript 的物件變數,本身並不像 c++ 那樣代表實體,
而是類似指標的參考,
當然這只是解釋的角度不同,都是在講同一個概念。
「傳值」或「傳址」對大多數的開發者來說應該都不陌生,那麼「psaa by sharing」又是什麼呢?
psaa → pass
已修正,感謝提醒
感謝大大分享。
這邊菜鳥新手有個小小心得,兩年前第一次看時,並未去讀 ECMA-262-3 in detail. Chapter 8. Evaluation strategy,加上只學過 JS,所以當時誤解成 (primitive -- Pass by value) + (object -- Pass by reference) = Pass by sharing。
這次再回過頭看一遍參考文章,才明白 Pass by reference 的定義。
不過這邊我的理解跟大大有稍微出入(不過只是名詞解釋的差異而已):
It can be either “call by value”, with specifying that the special case of call by value is meant — when the value is the address copy. From this position it is possible to say that everything in ECMAScript is passed by value.
Or, “call by sharing”, which makes this distinction from “by reference”, and “by value”. In this case it is possible to separate passing types: primitive values are passed by value and objects — by sharing.
看完這段的意思,我的理解是:
可以用以下這「兩者擇一」來定義 JS 的 strategy。
(1) “passed by value”(特殊情況時,value 是指 address copy)
(2) primitive 是 “passed by value” + objects 是 “passed by sharing”
如此 ”passed by sharing“ 應該只是指 objects 的部分。